Skip to content

[ADD] l10n_uy_edi_special_regime: CFE for Literal E companies - #443

Closed
jue-adhoc wants to merge 1 commit into
ingadhoc:19.0from
adhoc-dev:19.0-t-73403-jue
Closed

jue-adhoc wants to merge 1 commit into
ingadhoc:19.0from
adhoc-dev:19.0-t-73403-jue

Conversation

@jue-adhoc

@jue-adhoc jue-adhoc commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Qué hace

Nuevo módulo l10n_uy_edi_special_regime para que las compañías inscriptas en el régimen Literal E / IVA mínimo de DGI puedan emitir CFE válidos. Hoy DGI los rechaza con:

E05: Si el valor del CAE Especial es 2, 3 o 4 entonces el Ind. Mnt Bruto debe ser 3

Cuando la empresa está marcada como "Literal E o monotributo" en Uruware, el CFE se firma con CAE especial y DGI exige dos indicadores que l10n_uy_edi no contempla:

  • MntBruto = 3 (zona A10 del encabezado) — hoy el nodo ni se emite para líneas exentas.
  • IndFact = 16 (zona B4) en lugar de los indicadores de tasa de IVA (1 exento / 2 mínima / 3 básica / 4 otra). Los indicadores conceptuales se mantienen igual que en régimen general — confirmado por Uruware: 5 (entrega gratuita), 6/7 (no facturable: anticipos y líneas de descuento) y 10 (exportación).

Cómo

  • Campo l10n_uy_edi_taxpayer_regime en res.company (default general → el módulo es inerte salvo configuración explícita; sin auto_install).
  • Override de _l10n_uy_edi_cfe_A_iddoc() (MntBruto=3, solo CFE domésticos) y de _get_invoice_indicator() (mapea a 16 únicamente cuando el nativo devolvería un indicador de tasa). No se hereda el QWeb: el nodo MntBruto ya existe en cfe_template.xml, solo se completan los dicts.
  • Se extiende _l10n_uy_edi_check_move() para bloquear el envío si hay líneas con IVA a tasa distinta de 0%: cada rechazo de DGI quema un número de CAE (rechazo definitivo, sin reenvío ni NC).
  • Los impuestos configurados en Odoo no cambian (siguen 0% exento): la transformación a 16 ocurre solo al generar el XML, mismo criterio que el indicador de downpayments.

Aplica a e-Ticket (101), e-Factura (111) y sus NC/ND (102/103/112/113). Monotributo y Monotributo MIDES quedan fuera del selection hasta confirmar sus indicadores con Uruware (nota en README).

Tests

  • XML esperado completo (patrón expected_cfes de l10n_uy_edi) para los 6 tipos de comprobante en régimen Literal E.
  • Entrega gratuita mantiene IndFact 5; descuento global exento informa IndFactDR 16 (siguiendo los indicadores del detalle).
  • No-regresión doble: en régimen general y en CFE de exportación bajo Literal E, el XML se compara contra los expected nativos de l10n_uy_edi (20_e_ticket, 40_e_expo_invoice) — garantiza que el módulo no altera el comportamiento estándar.
  • Control positivo de la validación pre-envío (bloquea 22%, no bloquea exento).
  • Cada test nuevo se demostró en rojo con mutaciones quirúrgicas del gate antes de darlo por bueno.

Suite completa en verde: 0 failed, 0 error(s) of 11 tests.

Task: https://www.adhoc.inc/odoo/project.task/73403

@roboadhoc

Copy link
Copy Markdown
Contributor

Pull request status dashboard

@jue-adhoc
jue-adhoc force-pushed the 19.0-t-73403-jue branch 5 times, most recently from 3e3655b to f94589b Compare September 4, 2026 17:06
@pablohmontenegro

Copy link
Copy Markdown
Contributor

@jue-adhoc

  1. l10n_uy_edi_special_regime/models/account_move.py:43— El gate usais_invoice(), que incluyein_invoice/in_refund. El plan contable uruguayo ponel10n_latam_use_documents: Truetambién en el diario de compras (odoo/addons/l10n_uy/models/template_uy.py), así que una compañía Literal Eno puede confirmar ninguna factura de proveedor con IVA compras 22% (vat4):_postlevantaUserError. Debería limitarse ais_sale_document(), como hace el nativo en_compute_l10n_uy_edi_is_needed.
  2. models/account_move.py:59 — UserError dentro de _post aborta todo el batch. Posteo masivo desde la lista (o cron de facturas recurrentes) con un solo move no conforme rollbackea también los válidos. Peor: l10n_uy_edi/models/account_move.py:224 (to_post.action_post() dentro de l10n_uy_edi_action_update_dgi_state) re-postea CFE rechazados; si uno trae una línea al 22% revienta la actualización de estado DGI de todo el recordset.
  3. models/account_move.py:46 — El gate no excluye CFE de exportación (que el propio módulo declara "comportamiento estándar") ni diarios no electrónicos. Una e-Factura de exportación (121) que conserve el 22% (posición fiscal no aplicada, impuestos seteados a mano) queda bloqueada al confirmar, y en un diario l10n_uy_edi_type != 'electronic' se bloquea sin que exista CAE que quemar.
  4. models/account_move.py:27 — _get_invoice_indicator condiciona por company_id._l10n_uy_edi_is_special_regime() mientras el encabezado usa _l10n_uy_edi_apply_special_regime() (que sí excluye expo). Hoy queda tapado porque el nativo devuelve 10 antes del mapa de tasas, pero si se suma un código tipo exportación sin agregarlo a [121,122,123] de _l10n_uy_edi_is_expo_cfe, sale IndFact=16 sin MntBruto=3 — justo el rechazo E05 que el módulo quiere evitar. Usar el mismo helper en ambos lados.

@jue-adhoc
jue-adhoc force-pushed the 19.0-t-73403-jue branch 2 times, most recently from d203eed to 0b9375c Compare September 8, 2026 13:43
@jue-adhoc

jue-adhoc commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Aplicadas las 4 observaciones del review en 0b9375c:

  1. Alcance venta-only: el gate del bloqueo ahora usa l10n_uy_edi_is_needed (mismo compute que usa el nativo para decidir si el move va a ser CFE), que incluye is_sale_document() — las facturas de proveedor con IVA compras quedan fuera. Test nuevo: vendor bill con IVA 22% postea sin bloqueo.

  2. Batch posting: is_needed también excluye los moves con l10n_uy_edi_cfe_state seteado, así que el re-post de l10n_uy_edi_action_update_dgi_state (CFE accepted cancelados) ya no pasa por el chequeo. Para el posteo masivo, el UserError ahora acumula todos los moves ofensores (con su display_name) en un solo mensaje, en vez de fallar de a uno. El abort del batch ante un move inválido se mantiene a propósito — es el comportamiento estándar de las validaciones al postear y acá cada CFE mal emitido quema un número de CAE; si preferís degradar a revert-por-move en ese camino lo conversamos.

  3. Expo y diarios no electrónicos: is_needed trae el gate de journal_id.l10n_uy_edi_type == 'electronic', y la exclusión de exportación va por el helper compartido (para un expo con IVA ≠ 0% ya existe el chequeo nativo "Export CFE can only have 0% vat taxes"). Test nuevo: e-Factura de exportación con IVA 22 forzado postea sin que este módulo la bloquee.

  4. Helper único: _get_invoice_indicator ahora usa _l10n_uy_edi_apply_special_regime() igual que _l10n_uy_edi_cfe_A_iddoc — un solo lugar decide cuándo aplica el régimen, así un futuro código tipo exportación fuera de _l10n_uy_edi_is_expo_cfe no puede producir IndFact=16 sin MntBruto=3.

Suite: 12 tests en verde (se agregó test_85_check_move_scope cubriendo 1 y 3).

Extra

Pruebas funcionales de los últimos fixes + revalidar comportamiento original en esta build https://runbot.dev-adhoc.com/runbot/batch/107227/build/112004

- New module for companies under the DGI "Literal E / IVA mínimo" special
  regime, inert by default (no auto_install, default regime = general)
- Add l10n_uy_edi_taxpayer_regime selection field on res.company
- Override _l10n_uy_edi_cfe_A_iddoc to report MntBruto = 3 (zone A10):
  DGI requires it when Uruware signs the CFE with a special CAE (2, 3, 4)
- Override _get_invoice_indicator to report IndFact = 16 (zone B4) where
  a VAT rate indicator (1/2/3/4) would be used. Per Uruware, conceptual
  indicators are kept: 5 (free delivery), 6/7 (non billable, e.g. down
  payments and discount lines) and 10 (exports)
- Block CFEs with VAT rates other than 0% as early as possible (each CFE
  rejected by DGI burns a CAE number): UserError on _post, and the same
  check on _l10n_uy_edi_check_move for the send path
- Tests: expected XMLs for e-Ticket / e-Invoice and their credit/debit
  notes under the special regime, free delivery (keeps IndFact 5),
  global discount (IndFactDR 16), no-regression tests asserting that
  the general regime and export CFEs stay identical to the l10n_uy_edi
  standard XMLs, and tests for the post/pre-send validation

Change note: Las compañías uruguayas inscriptas en el régimen Literal E (IVA mínimo) ahora pueden emitir comprobantes electrónicos válidos desde Odoo: hasta ahora DGI los rechazaba por no incluir los indicadores que exige ese régimen. Se configura desde el nuevo campo "Régimen de contribuyente DGI" en la compañía (además del ajuste correspondiente en Uruware). Si por error se cargan impuestos con IVA distinto de 0%, el sistema avisa al usuario al confirmar la factura para corregirlo antes de enviar nada, evitando rechazos que inutilizan numeración. Los descuentos, entregas gratuitas, anticipos y exportaciones siguen informándose como siempre, según lo confirmado por Uruware.
@pablohmontenegro

Copy link
Copy Markdown
Contributor

@roboadhoc nobump r+

@pablohmontenegro

pablohmontenegro commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Mezclo debido a la urgencia pero dejo estos comentarios a evaluar por si corresponde hacer fixes: @jue-adhoc

  1. CFE de exportación quedan afuera del MntBruto=3 — criticidad: ALTA (a confirmar con Uruware)
    account_move.py:14 — Se excluyen los CFE de exportación del MntBruto=3, pero el flag "Literal E o monotributo" de Uruware es a nivel compañía: un CFE de exportación (121) igual sale con CAE especial y debería pegar el mismo rechazo E05 de DGI, quemando un CAE. Requiere confirmación con Uruware antes de mergear — si se confirma, es un bug funcional que quema numeración.
  2. Totales de la sección C sin tocar — criticidad: ALTA
    account_move.py:22 — Las líneas ahora reportan IndFact 16 pero Totales sigue poniendo el monto en MntNoGrv (A112, el bucket de IndFact=1). Es visible en los XML esperados del propio PR. Si DGI cruza los subtotales por indicador, el CFE pasa a rechazarse por totales justo después de haber arreglado los indicadores. Inconsistencia interna del XML.
  3. e-Remito (181) sin override — criticidad: MEDIA
    account_move.py:16 — Los CFE de guía de remisión se construyen desde stock.picking con MntBruto = None hardcodeado en l10n_uy_edi_stock, y no reciben ningún override acá. El gap E05 sigue abierto para compañías que usan remitos. Alcance incompleto más que bug del código escrito.
  4. _post valida todo self en lugar del subset soft=True — criticidad: BAJA/MEDIA
    account_move.py:65 — Con soft=True, un borrador infractor con fecha futura (que no se iba a postear en esta pasada) aborta el posteo de movimientos válidos del mismo batch. Se arregla filtrando por el mismo criterio que usa _post para decidir qué postea.

@roboadhoc roboadhoc closed this in 3ddad35 Sep 8, 2026
@roboadhoc
roboadhoc deleted the 19.0-t-73403-jue branch September 8, 2026 17:44
@roboadhoc roboadhoc added the 18.1 label Sep 8, 2026
@jue-adhoc

jue-adhoc commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Mezclo debido a la urgencia pero dejo estos comentarios a evaluar por si corresponde hacer fixes: @jue-adhoc

  1. CFE de exportación quedan afuera del MntBruto=3 — criticidad: ALTA (a confirmar con Uruware)
    account_move.py:14 — Se excluyen los CFE de exportación del MntBruto=3, pero el flag "Literal E o monotributo" de Uruware es a nivel compañía: un CFE de exportación (121) igual sale con CAE especial y debería pegar el mismo rechazo E05 de DGI, quemando un CAE. Requiere confirmación con Uruware antes de mergear — si se confirma, es un bug funcional que quema numeración.
  2. Totales de la sección C sin tocar — criticidad: ALTA
    account_move.py:22 — Las líneas ahora reportan IndFact 16 pero Totales sigue poniendo el monto en MntNoGrv (A112, el bucket de IndFact=1). Es visible en los XML esperados del propio PR. Si DGI cruza los subtotales por indicador, el CFE pasa a rechazarse por totales justo después de haber arreglado los indicadores. Inconsistencia interna del XML.
  3. e-Remito (181) sin override — criticidad: MEDIA
    account_move.py:16 — Los CFE de guía de remisión se construyen desde stock.picking con MntBruto = None hardcodeado en l10n_uy_edi_stock, y no reciben ningún override acá. El gap E05 sigue abierto para compañías que usan remitos. Alcance incompleto más que bug del código escrito.
  4. _post valida todo self en lugar del subset soft=True — criticidad: BAJA/MEDIA
    account_move.py:65 — Con soft=True, un borrador infractor con fecha futura (que no se iba a postear en esta pasada) aborta el posteo de movimientos válidos del mismo batch. Se arregla filtrando por el mismo criterio que usa _post para decidir qué postea.
  1. No estoy segura, no estaba especificado el comportamienot de comprobantes de expo, estaban como no alcanzados. Confirmo con Uruware y subo los cambios de ser necesario.
  2. No hay que cambiar totales, lo dice la misma spec de la tarea en el titulo "Lo que NO hay que tocar"
  3. El MntBruto = None hardcodeado de l10n_uy_edi_stock es correcto por norma. En la matriz de obligatoriedad, A-C10 está marcado "no corresponde" (0) tanto para e-Remito como para e-Remito de Exportación
  4. Puede llegar a darse, pero es muy poco frecuente. Si surge lo corregimos.

This branch was previously deployed

1 inactive deployment
merge — e965bca4 Deployed Sep 8, 2026 by roboadhoc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants